/**
 * OpenLayers 3 Layer Switcher Control.
 * 
 * Il controllo LayerSwitcher visualizza una tabella di contenuti per la mappa. In questo modo l'utente può per i layer di Base passare da un
 * BaseLayers ad un'altro.
 * Mentre, per i layer non di base, mostrare o nascondere i livelli e consentirne il riordino. 
 * Per impostazione predefinita, lo switcher è mostrato minimizzato sul bordo destro della mappa, l'utente può espandere cliccando sulla maniglia.
 * 
 * I layer tutti per essere considerati dal componente devono avere la proprietà "displayInLayerSwitcher: true" nell'oggetto ol.layer.Layer.
 * Un layer per essere considerato di base, deve prevedere la proprietà "type: 'base'" nell'oggetto ol.layer.Layer.
 * <br />
 * <p>Esempio: 
 *   		new ol.layer.Tile({ 
 *   			 title: 'OSM', <strong>type: 'base'</strong>, visible: true, source: new ol.source.OSM()
 *   		})
 * </p>
 * <br />
 * 
 * I layer group, di base e non, devono prevedere la proprietà "combine: true" nell'oggetto ol.layer.Group.
 * 
 * <br />
 * <p>Esempio: 
 *   		new ol.layer.Group({ 
 *   				title: 'Satellite and labels',
 *   				<strong>type: 'base',</strong> // se un layer di base
 *   				<strong>combine: true,</strong>
 *   				visible: false,
 *   				layers: [
 *                       new ol.layer.Tile({
 *                       	source: new ol.source.BingMaps({
 *                              key: '.....',
 *                              imagerySet: 'Aerial'
 *                          })
 *                       }),
 *                       new ol.layer.Tile({
 *                          source: new ol.source.Stamen({
 *                               layer: 'terrain-labels'
 *                          })
 *                       })
 *                  ]
 *         })
 * </p>
 * <br />
 * 
 * I layer tutti per il corretto funzionamento, devono prevedere la proprietà "name" intesa come codice layer e "title" intesa come nome del 
 * layer visualizzato dall'utente, nell'oggetto ol.layer.Layer.
 * 
 * @constructor
 * @extends {ol.control.Control}
 * @param {Object} opt_options Control options, extends olx.control.ControlOptions adding:
 *  
 *     Property	   | 			Type		   |		Descrizione
 *-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 *   collapsed	   |    boolean - undefined	   |  Se il layer panel dovrebbe iniziare espanso o meno. Predefinito true.
 *   collapsible   |    boolean - undefined    |  Se il layer panel può essere nascosto o meno. Predefinito true.
 *   hasOverlay    | 	boolean - undefined    |  Se nel layer panel dovrebbero essere gestiti anche i layer non di base. Predefinito true.
 *   reordering    | 	boolean - undefined    |  Se per i layer non di base dovrebbero essere consentito il riordino dei livelli. Predefinito true.
 *   chVisible 	   |	boolean - undefined	   |  Se per i layer non di base dovrebbero essere consentito di cambiarne la visibilità. Predefinito false.
 *   className	   |	string - undefined	   |  CSS class name. Default is layer-switcher.
 *   label		   | string - Node - undefined |  Testo dell'etichetta da utilizzare per il pulsante quando il layer panel è nascosto. Il valore predefinito è '+'. Invece di testo, anche un elemento Html può essere utilizzato.
 *   tipLabel  	   |	string - undefined 	   |  Testo dell'etichetta da utilizzare per il tooltip del pulsante. Il valore predefinito è "Layer Switcher".
 *   baseLabel     | string - Node - undefined |  Testo dell'etichetta da utilizzare per identificare i layer di base. Il valore predefinito è 'Base Layer'. Invece del testo, anche un elemento Html può essere utilizzato.
 *   overlayLabel  | string - Node - undefined |  Testo dell'etichetta da utilizzare per identificare i layer non di base. Il valore predefinito è 'Overlay'. Invece del testo, anche un elemento Html può essere utilizzato.
 *   collapseLabel | string - Node - undefined |  Testo dell'etichetta da utilizzare per il pulsante quando il layer panel è espanso. Il valore predefinito è '-'. Invece del testo, anche un elemento Html può essere utilizzato.
 *   placementTip  | 	string - undefined	   |  In quale direzione il tooltip deve essere mostrato. Supporta left, right, top, and bottom. Predefinito è left.
 */
mycontrol.LayerSwitcher = function(opt_options) {

    var options = opt_options || {};
    
    /**
     * Listeners associated with the map 
     * @private
     */
    this.mapListeners = [];
  
    /**
     * Raccolta dei layer non di base, al momento, gestiti dallo switcher 
     * @private
     */
    this.layerCache = {};
  
 /*************************************************************************************************************************/
 /*												 INIZIALIZZAZIONE LABEL	DI DEFAULT										  */
 /*************************************************************************************************************************/
   
    /* DEFAULT: Testo dell'etichetta da utilizzare per il pulsante quando il layer panel è espanso. */
    var collapseLabel_ = document.createElement("i");
    	collapseLabel_.setAttribute("class", "fa fa-minus");
    	collapseLabel_.setAttribute("aria-hidden", true);
    
    /* DEFAULT: Testo dell'etichetta da utilizzare per il pulsante quando il layer panel è nascosto */
    var label_ = document.createElement("i");
        label_.setAttribute("class", "fa fa-plus");
        label_.setAttribute("aria-hidden", true);
    	    
    /* DEFAULT: Testo dell'etichetta da utilizzare per identificare i layer di base. */
    var baseLabel_ = document.createElement('label');
	    baseLabel_.innerHTML = 'Base Layer';
	    baseLabel_.className = "base";
	    
	/* DEFAULT: Testo dell'etichetta da utilizzare per identificare i layer non di base. */
	var overlayLabel_ = document.createElement('label');
		overlayLabel_.innerHTML = 'Overlay';
		overlayLabel_.className = "overlay";
       
 /*************************************************************************************************************************/
 /*											 	MERGE OPTIONS DEFAULT - INSTANCE 										  */
 /*************************************************************************************************************************/
		
	/* Determinare se il layer panel è espanso. */
	this.collapsed_ = options.collapsed !== undefined ? options.collapsed : false;

	
	/* Impostare se il layer panel dovrebbe essere comprimibile. */
	this.collapsible_ = options.collapsible !== undefined ? options.collapsible : true;
	if (!this.collapsible_) this.collapsed_ = false;
	
	
    /* Title button */
    var tipLabel = options.tipLabel !== undefined ? options.tipLabel : 'Layer Switcher';

    
    /* Css Class */
    var className = options.className !== undefined ? options.className : 'layer-switcher';
    /* Controllo se Applicare la soluzione per consentire lo scorrimento dei contenuti all'interno di un traboccante */
    if (mycontrol.LayerSwitcher.isTouchDevice_()) className += ' touch';
    
    
    /* Label button quando il layer panel è espanso */
    var collapseLabel = options.collapseLabel !== undefined ? options.collapseLabel : collapseLabel_;

    if (typeof collapseLabel === 'string') {
       /**
        * @private
        * @type {Node}
        */
       this.collapseLabel_ = document.createElement('span');
       this.collapseLabel_.textContent = collapseLabel;
    } else {
       this.collapseLabel_ = collapseLabel;
    }
    

    /* Label button quando il layer panel è collassato */
    var label = options.label !== undefined ? options.label : label_;
    
    if (typeof label === 'string') {
       /**
        * @private
        * @type {Node}
        */
       this.label_ = document.createElement('span');
       this.label_.textContent = label;
    } else {
       this.label_ = label;
    }
    
    
    /* Label dell'header che individua il gruppo dei layer di base */
    var baseLabel = options.baseLabel !== undefined ? options.baseLabel : baseLabel_;
    
    if (typeof baseLabel === 'string') {
    	/**
         * @private
         * @type {Node}
         */
        this.baseLabel_ = baseLabel_;
        this.baseLabel_.innerHTML = baseLabel;
    } else {
        this.baseLabel_ = baseLabel;
    }
    
    
    /* Impostare se nel layer panel dovrebbero essere gestiti anche i layer non di base. */
    this.hasOverlay_ = options.hasOverlay !== undefined ? options.hasOverlay : true;
    
    if(this.hasOverlay_){
    	
    	/* Impostare se per i layer non di base dovrebbero essere consentito il riordino dei livelli. */
        this.reordering_ = options.reordering !== undefined ? options.reordering : true;
        
        /* Impostare se per i layer non di base dovrebbero essere consentito di cambiarne la visibilità. */
        this.chVisible_ = options.chVisible !== undefined ? options.chVisible : false;
        
	    /* Label dell'header che individua il gruppo dei layer non di base */
	    var overlayLabel = options.overlayLabel !== undefined ? options.overlayLabel : overlayLabel_;
	    
	    if (typeof overlayLabel === 'string') {
	    	/**
	         * @private
	         * @type {Node}
	         */
	        this.overlayLabel_ = overlayLabel_;
	        this.overlayLabel_.innerHTML = overlayLabel;
	    } else {
	       this.overlayLabel_ = overlayLabel;
	    }
    }
    
 /*************************************************************************************************************************/
 /*											 			CONTAINER WIDGET												  */
 /*************************************************************************************************************************/
   
    /* Determinare la label del button (per mostrare/nascondere il panello) di partenza */
    var activeBtnLabel = (this.collapsible_ && this.collapsed_) ? this.collapseLabel_ : this.label_;
    
    /*
     * Button per mostrare e nascondere il layer panel
     */
    this.button = document.createElement('button');
    this.button.setAttribute('type', 'button');
    this.button.title = tipLabel;
    this.button.appendChild(activeBtnLabel);
    	  
    /*
     * Layer Panel
     */	
    this.panel = document.createElement('div');
    this.panel.className = 'panelSw';
    	
    /* List ul layer overlay */
	this.ulOverlay = document.createElement('ul');
	this.ulOverlay.className = "list-unstyled overlay " + ( this.reordering_ ? "sort" : "unsort");
	
	/* List ul layer base */
	this.ulBase = document.createElement('ul');
	this.ulBase.className = "list-unstyled base";
    	
    /* Set active css container control */
    var cssClassesActive = className + ' ol-unselectable ol-control ' + (this.collapsed_ && this.collapsible_ ? ' ol-collapsed' : '') + (this.collapsible_ ? '' : ' ol-uncollapsible');

    /*
     * Container control element 
     */
    var element = document.createElement('div');
		element.className = cssClassesActive;
		element.appendChild(this.panel);
		element.appendChild(this.button);
    
	/*
	 * Applicare soluzione per consentire lo scorrimento dei contenuti all'interno
	 */
    mycontrol.LayerSwitcher.enableTouchScroll_(this.panel);

    var this_ = this;

    /*
     * Gestione click button
     */
    this.button.onclick = function(e) {
        e = e || window.event;
        e.preventDefault();
        
        this_.handleToggle_();
    };
    /*
     * Gestione click button per mobile
     */
    this.button.ontouchstart = function(e) {
    	e = e || window.event;
    	e.preventDefault();
    	
    	this_.handleToggle_();
    };

    ol.control.Control.call(this, {
        element: element,
        target: options.target
    });
};

ol.inherits(mycontrol.LayerSwitcher, ol.control.Control);

/**
 * Collassare o espandere il layer panel in base al parametro 'collapsed_'. La funzione metodo non farà nulla 
 * se il layer panel non è comprimibile.
 * 
 * @private
 */
mycontrol.LayerSwitcher.prototype.handleToggle_ = function() {
	if(this.collapsible_){
		this.element.classList.toggle('ol-collapsed');
		
		if (this.collapsed_) {
			/**
			 * HIDE the layer panel.
			 */
			this.button.removeChild(this.button.childNodes[0]);
			this.button.appendChild(this.label_);
		} else {
			/**
			 * SHOW the layer panel.
			 */
			this.button.removeChild(this.button.childNodes[0]);
			this.button.appendChild(this.collapseLabel_);
		}
		this.collapsed_ = !this.collapsed_;
   }//
};

/**
 * Disegna il contenuto del layer panel per rappresentare lo stato attuale degli strati.
 */
mycontrol.LayerSwitcher.prototype.renderPanel = function() {

	/*
	 * Assicurarsi che solo uno dei layer di base sia visibile quando più di uno è stato istanziato con visibilità a true.
	 */
    this.ensureTopVisibleBaseLayerShown_();

    while(this.panel.firstChild) {
        this.panel.removeChild(this.panel.firstChild);
    }
    
	/* Container dei due tag <ul> */
    var divBase = document.createElement('div');	
    	divBase.className = "text-center";
    
		divBase.appendChild(this.baseLabel_);
    	divBase.appendChild(this.ulBase);
    	
    	if(this.hasOverlay_){
    		divBase.appendChild(this.overlayLabel_);
    		divBase.appendChild(this.ulOverlay);
    	}
    
    /* append al panel il container appena creato */
    this.panel.appendChild(divBase);
    
    /* Render all layers that are children of a group. */
    this.renderLayers_(this.getMap(), this.ulBase, this.ulOverlay);
    
    var this_ = this;
    
    /*
     * Controllo se per i layer non di base è attiva la gestione. 
     */
    if(this.hasOverlay_){
    	/*
         * Controllo se per i layer non di base dovrebbero essere consentito il riordino dei livelli, se si avvio l'ordinamento
         * al tag <ul> container dell'elenco dei layer non di base. 
         */
    	if(this.reordering_){
    		/* Sorting */
		    $(this.ulOverlay).sortable({
		    	/*
		    	 * This event is triggered when the user stopped sorting and the DOM position has changed.
		    	 */
		    	update: function( event, ui ) {
		    		/* Refresh the sortable items. Triggers the reloading of all sortable items, causing new items to be recognized. */
		    		$(this).sortable( "refreshPositions" );
		    		
		    		/* Serializes the sortable's item id's into an array of string. 
		    		 * Parametri:
		    		 *  options Type: Object - Options to customize the serialization.
		    		 *  attribute (default: "id") Type: String - The name of the attribute to use for the values.
		    		 */
		    		var toArray = $( this ).sortable( "toArray", {'attribute':'data-layer'} );
		    		
		    		if(toArray != undefined && toArray != null && toArray.length != 0)
		    			/* Ciclo l'array con i codici layer, per ciascuno ridefinisco lo z-index impostandolo uguale alla posizione nell'array */
			    		$.each(toArray, function( index, codLayer ) {
			    			/* recupero il layer */
			    			var lyr = this_.getMap().getLayerByName(codLayer);
			    			/* set z-index */
			    			if(lyr != null) lyr.setZIndex((toArray.length -1) - index);
			    		});
		    	}// close update
		    });
    	}// has reordering_
	    /*
	     * Registro l'evento alla mappa per cui ogni volta che la collection dei layer aggiunti alla mappa cambia viene invocato.
	     * In questo modo, posso mantenere aggiornata la lista dei layer nel panel.
	     */
    	this.mapListeners.push(this.getMap().getLayerGroup().on('change', this.fireChange_, this));
    }// hasOverlay
};

/**
 * Evento invocato ogni volta che la collection dei layer aggiunti alla mappa cambia.
 * In questo modo, posso mantenere aggiornata la lista dei layer nel panel.
 * 
 * @private
 */
mycontrol.LayerSwitcher.prototype.fireChange_ = function(e) {
	
	if (!this.getMap()) return;
	
	var this_ = this;

	/*
	 * Ciclo i layer in cache e per ciascuno verifico se il layer è ancora presente in mappa, se no vuol dire che è stato 
	 * rimosso dalla mappa, procedo con il rimuovere il corrispondente <li> dalla view e dalla cache.
	 */
	for ( var key in this_.layerCache) {
		var lyr = this_.layerCache[key];
		
		if(this_.getMap().getLayerByName(lyr.getName()) == null) {
			/* rimuovo il <li> */
			$(this_.ulOverlay).find("li[data-layer=" + lyr.getName() + "]").remove();
			/* rimuovo l'elemento dalla cache */
			delete this_.layerCache[key];
		}
	}
	
	/*
	 * Ciclo i layer aggiunti alla mappa e per ciascuno verifico se il layer è presente nella cache, se no vuol dire che è stato 
	 * aggiunto alla mappa, procedo con l'aggiungere il corrispondente <li> alla view e alla cache.
	 */
	this_.getMap().getLayers().forEach(function(lyr, idx, a) {
		if (lyr.get('displayInLayerSwitcher') && lyr.get('type') != 'base' && lyr.get('title') && this_.layerCache[lyr.getName()] == undefined) {
			/* aggiungo il <li> */
			this_.ulOverlay.appendChild(this_.renderLayer_(lyr, idx));
			/* aggiungo l'elemento alla cache */
			this_.layerCache[lyr.getName()] = lyr;
		}
    });
	
	if($(this_.ulOverlay).find('li').length != 0){
	   	$(this_.overlayLabel_).attr("style", "display:block;");
	}else
	   	$(this_.overlayLabel_).attr("style", "display:none;");
	 
	/* Refresh the sortable items */
	if(this_.reordering_) $(this_.ulOverlay).sortable( "refresh" );
};

/**
 * Set the map instance the control is associated with.
 * @param {ol.Map} map The map instance.
 */
mycontrol.LayerSwitcher.prototype.setMap = function(map) {
	var oldMap = this.getMap();

	if (map === oldMap) return;
	  
    /* Clean up listeners associated with the previous map */
    for (var i = 0, key; i < this.mapListeners.length; i++) {
    	oldMap.unByKey(this.mapListeners[i]);
    }
    this.mapListeners.length = 0;
	
    // Wire up listeners etc. and store reference to new map
    ol.control.Control.prototype.setMap.call(this, map);
    
    if (map) {
    	/*
    	 * Ri-Disegno il contenuto del layer panel per rappresentare lo stato attuale degli strati.
    	 */
        this.renderPanel();
    }
};

/**
 * Assicurarsi che solo uno dei layer di base sia visibile quando più di uno è stato istanziato con visibilità a true.
 * 
 * @private
 */
mycontrol.LayerSwitcher.prototype.ensureTopVisibleBaseLayerShown_ = function() {
    var lastVisibleBaseLyr;
    mycontrol.LayerSwitcher.forEachRecursive(this.getMap(), function(l, idx, a) {
        if (l.get('type') === 'base' && l.getVisible()) {
            lastVisibleBaseLyr = l;
        }
    });
    if (lastVisibleBaseLyr) this.setVisible_(lastVisibleBaseLyr, true);
};

/**
 * Toggle the visible state of a layer.
 * Takes care of hiding other layers in the same exclusive group if the layer is toggle to visible.
 * 
 * @param {ol.layer.Base} The layer whos visibility will be toggled.
 * 
 * @private
 */
mycontrol.LayerSwitcher.prototype.setVisible_ = function(lyr, visible) {
    var map = this.getMap();
    lyr.setVisible(visible);
    if (visible && lyr.get('type') === 'base') {
        // Hide all other base layers regardless of grouping
    	mycontrol.LayerSwitcher.forEachRecursive(map, function(l, idx, a) {
            if (l != lyr && l.get('type') === 'base') {
                l.setVisible(false);
            }
        });
    }
};

/**
 * Rendering di tutti i livelli che sono figli di un gruppo.
 * @private
 * @param {ol.layer.Base} lyr Layer to be rendered (should have a title property).
 * @param {Number} idx Position in parent group list.
 */
mycontrol.LayerSwitcher.prototype.renderLayer_ = function(lyr, idx) {

    var this_ = this;

    var li = document.createElement('li');

    var lyrTitle = lyr.get('title');
    var lyrId = mycontrol.LayerSwitcher.uuid();

    var label = document.createElement('label');

    if (lyr.getLayers && !lyr.get('combine')) {

        li.className = 'group';
        label.innerHTML = lyrTitle;
        li.appendChild(label);
        var ul = document.createElement('ul');
        li.appendChild(ul);

        this.renderLayers_(lyr, ul);

    } else {

        li.className = 'layer';
        var input = document.createElement('input');
        if (lyr.get('type') === 'base') {
            input.type = 'radio';
            input.name = 'base';
       
	        input.id = lyrId;
	        input.checked = lyr.get('visible');
	        input.onchange = function(e) {
	            this_.setVisible_(lyr, e.target.checked);
	        };
	        li.appendChild(input);
        } else {
        	
        	if(this.reordering_){
	        	var span = document.createElement('span');
	    			span.setAttribute("class", "sortable-handle fa fa-reorder");
	    		li.appendChild(span);
        	}
        	li.setAttribute('data-layer', lyr.getName());
        	
        	/* Aggiungo l'elemento alla cache */
        	this_.layerCache[lyr.getName()] = lyr; 
        	
        	/* Controllo se per i layer non di base, è attiva l'opzione che consente di cambiare la visibilita del layer */
        	if(this.chVisible_){
	            input.type = 'checkbox';
	            
	            input.id = lyrId;
		        input.checked = lyr.get('visible');
		        input.onchange = function(e) {
		            this_.setVisible_(lyr, e.target.checked);
		        };
		        li.appendChild(input);
        	}
        }
        label.htmlFor = lyrId;
        label.innerHTML = lyrTitle;
        li.appendChild(label);
    }

    return li;
};

/**
 * Render all layers that are children of a group.
 * @private
 * @param {ol.layer.Group} lyr Group layer whos children will be rendered.
 * @param {Element} elm DOM element that children will be appended to.
 */
mycontrol.LayerSwitcher.prototype.renderLayers_ = function(lyr, elm, elm_) {
    var lyrs = lyr.getLayers().getArray().slice().reverse();
    for (var i = 0, l; i < lyrs.length; i++) {
        l = lyrs[i];
        if (l.get('displayInLayerSwitcher') && l.get('title')) {
            if(l.get('type') === "base") 
            	elm.appendChild(this.renderLayer_(l, i));
            else
                elm_.appendChild(this.renderLayer_(l, i));
        }
    }
    
    if($(this.ulOverlay).find('li').length != 0){
    	$(this.overlayLabel_).attr("style", "display:block;");
    }else
    	$(this.overlayLabel_).attr("style", "display:none;");
};

/**
 * **Static** Call the supplied function for each layer in the passed layer group
 * recursing nested groups.
 * @param {ol.layer.Group} lyr The layer group to start iterating from.
 * @param {Function} fn Callback which will be called for each `ol.layer.Base`
 * found under `lyr`. The signature for `fn` is the same as `ol.Collection#forEach`
 */
mycontrol.LayerSwitcher.forEachRecursive = function(lyr, fn) {
    lyr.getLayers().forEach(function(lyr, idx, a) {
        fn(lyr, idx, a);
        if (lyr.getLayers) {
        	mycontrol.LayerSwitcher.forEachRecursive(lyr, fn);
        }
    });
};

/**
 * Generate a UUID
 * @returns {String} UUID
 *
 * Adapted from http://stackoverflow.com/a/2117523/526860
 */
mycontrol.LayerSwitcher.uuid = function() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
        return v.toString(16);
    });
}

/**
 * @private
 * @desc Apply workaround to enable scrolling of overflowing content within an
 * Adapted from https://gist.github.com/chrismbarr/4107472
 */
mycontrol.LayerSwitcher.enableTouchScroll_ = function(elm) {
   if(mycontrol.LayerSwitcher.isTouchDevice_()){
       var scrollStartPos = 0;
       elm.addEventListener("touchstart", function(event) {
           scrollStartPos = this.scrollTop + event.touches[0].pageY;
       }, false);
       elm.addEventListener("touchmove", function(event) {
           this.scrollTop = scrollStartPos - event.touches[0].pageY;
       }, false);
   }
};

/**
 * @private
 * @desc Determine if the current browser supports touch events. Adapted from
 * https://gist.github.com/chrismbarr/4107472
 */
mycontrol.LayerSwitcher.isTouchDevice_ = function() {
    try {
        document.createEvent("TouchEvent");
        return true;
    } catch(e) {
        return false;
    }
};
